﻿// #define DEBUG_AGENT
#if !DEBUG && DEBUG_AGENT
Remove DEBUG_AGENT flag for RELEASE
#endif

using EmperialApps.WeatherSpark.Agent;
using EmperialApps.WeatherSpark.Data;
using Microsoft.Phone.Scheduler;
using Microsoft.Phone.Shell;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Windows.Media;

namespace EmperialApps.WeatherSpark.Internal {

    internal sealed partial class Settings {

        public const int MaximumCount = 5;

        public const string UniquifierName = "Uniquifier";
        public const string ServerStorage = "/weatherspark.blob.core.windows.net/";
        public const string Corrections = "http://weatherspark.azurewebsites.net/corrections.txt";

        public static readonly Uri SettingsPage = new Uri( "/SettingsPage.xaml", UriKind.Relative );
        public static readonly Uri AdvancedSettingsPage = new Uri( "/AdvancedSettingsPage.xaml", UriKind.Relative );
        public static readonly Uri ChooseLocationPage = new Uri( "/ChooseLocationPage.xaml", UriKind.Relative );
        public static readonly Uri AboutPage = new Uri( "/YourLastAboutDialog;component/AboutPage.xaml", UriKind.Relative );
        public static readonly Uri DetailPage = new Uri( "/DetailPage.xaml", UriKind.Relative );
        public static readonly TimeSpan UpdateLimit = TimeSpan.FromMinutes( 25 );


        /// <summary>Gets or sets the settings for the current location.</summary>
        public static LocationSettings Current { get; set; }

        /// <summary>Gets a unique ID for push network identification.</summary>
        public static Guid UniqueId {
            get {
                var uniqueIdSetting = Ensure( ref _uniqueId, "guid" );
                if( uniqueIdSetting.Value == default( Guid ) )
                    uniqueIdSetting.Value = Guid.NewGuid( );

                return uniqueIdSetting.Value;
            }
        }

        /// <summary>Gets or sets an incrementing value used to generate unique URLs.</summary>
        public static uint Uniquifier {
            get { return Ensure( ref _uniquifier, "uniquifier" ).Value; }
            set { Ensure( ref _uniquifier, "uniquifier" ).Value = value; }
        }

        /// <summary>Gets the default unit value for the next location, based on the last used value.</summary>
        public static Units DefaultUnits {
            get { return Ensure( ref _defaultUnits, "default_units" ).Value; }
            private set { Ensure( ref _defaultUnits, "default_units" ).Value = value; }
        }

        /// <summary>Gets the maximum number of forecasts that can be added in trial mode.</summary>
        public static int MaximumTrialCount {
            get { return Ensure( ref _maximumTrialCount, "max_trial_count" ).Value; }
            private set { Ensure( ref _maximumTrialCount, "max_trial_count" ).Value = value; }
        }

        /// <summary>Gets or sets the background color of tile images.</summary>
        public static uint TileBackgroundColorValue {
            get { return Ensure( ref _tileBackgroundColor, "tile_background_color" ).Value; }
            set { Ensure( ref _tileBackgroundColor, "tile_background_color" ).Value = value; }
        }

        /// <summary>Gets or sets a value indicating whether automatic tile re-scheduling should be disabled for debugging.</summary>
        public static bool DisableTileRescheduling {
            get { return Ensure( ref _disableTileRescheduling, "disable_tile_rescheduling" ).Value; }
            set { Ensure( ref _disableTileRescheduling, "disable_tile_rescheduling" ).Value = value; }
        }

        /// <summary>Gets or sets a value indicating whether automatic refresh of forecasts should be disabled for debugging.</summary>
        public static bool DisableForecastAutoRefresh {
            get { return Ensure( ref _disableForecastAutoRefresh, "disable_forecast_auto_refresh" ).Value; }
            set { Ensure( ref _disableForecastAutoRefresh, "disable_forecast_auto_refresh" ).Value = value; }
        }

        /// <summary>Gets the number of saved locations.</summary>
        public static int Count {
            get { return LocationNames.Length; }
        }


        /// <summary>Gets the accent brush for the application.</summary>
        public static SolidColorBrush GetAccentBrush( ) {
            return (SolidColorBrush)App.Current.Resources["PhoneAccentBrush"];
        }

        /// <summary>Gets a value indicating whether this is the user's first forecast download.</summary>
        public static bool IsFirstForecast( ) {
            var previousSetting = Ensure( ref _hasPreviousForecasts, "previous" );
            bool first = !previousSetting.Value;
            previousSetting.Value = true;
            return first;
        }

        /// <summary>Gets the index of the location settings with the specified coordinates.</summary>
        public static int IndexOfLocation( Coordinate location ) {
            string name = location.GetName( );
            int index = Array.IndexOf( LocationNames, name );
            return index;
        }

        /// <summary>Gets the location settings at the specified index.</summary>
        public static LocationSettings GetLocationSettings( int index ) {
            var container = GetLocationSettingsContainer( index );

            // If settings have been lost, re-created based on location name.
            if( container.Value == null ) {
                typeof( Settings ).Log( "Repairing location settings " + index );
                string locationName = LocationNames[index];
                Coordinate location = Coordinate.Parse( locationName );
                string displayName = location.GetGeographicName( );
                container.Value = new LocationSettings( location, displayName, default( ForecastSourceId ), null );

                // If location had a tile, use values stored for tile agent.
                int tileId;
                ForecastInfo info;
                if( _agentSettings.TryGetValue( location, out info )
                 && ForecastInfo.TryGetTileId( info.Identifier, out tileId ) )
                    container.Value.RecoverSettings( info, tileId );
            }
            else {
                // If tile agent has updated forecast recently, use latest time value.
                ForecastInfo info;
                Coordinate location = container.Value.Location;
                _agentSettings.TryGetValue( location, out info );
                if( container.Value.LastUpdate < info.LastUpdate )
                    container.Value.LastUpdate = info.LastUpdate;
            }


            return container.Value;
        }

        /// <summary>Gets all saved location settings.</summary>
        public static IEnumerable<LocationSettings> GetAllLocationSettings( ) {
            return Enumerable.Range( 0, Count )
                .Select( GetLocationSettings );
        }

        /// <summary>Adds the specified settings to the list of saved locations.</summary>
        public static void AddLocation( LocationSettings locationSettings ) {
            typeof( Settings ).Log( "Adding location settings " + LocationNames.Length );
            LoadAllSettingsContainers( );

            string name = locationSettings.Location.GetName( );
            string[] locationNames = new string[LocationNames.Length + 1];
            LocationNames.CopyTo( locationNames, 0 );
            locationNames[LocationNames.Length] = name;
            var container = CreateLocationSettingsContainer( name, locationSettings );

            _settings.Add( container );
            LocationNames = locationNames;
        }

        /// <summary>Changes the location of the specified settings.</summary>
        public static bool MoveLocation( LocationSettings locationSettings, Coordinate newLocation ) {
            Coordinate oldLocation = locationSettings.Location;
            if( oldLocation == newLocation )
                return false;

            int index = IndexOfLocation( oldLocation );
            typeof( Settings ).Log( "Moving location settings " + index );

            var oldContainer = GetLocationSettingsContainer( index );
            string[] locationNames = LocationNames.ToArray( );
            string newName = newLocation.GetName( );
            locationNames[index] = newName;

            LocationSettings.InitializeLocation( locationSettings, newLocation );
            _settings[index] = CreateLocationSettingsContainer( newName, locationSettings );
            LocationNames = locationNames;
            oldContainer.Value = null;
            oldLocation.DeleteFile( typeof( Settings ), index );

            locationSettings.UpdateTile( null, TileUpdateReason.Refresh );
            return true;
        }

        /// <summary>Removes the settings from the list of saved locations.</summary>
        public static void RemoveLocation( LocationSettings locationSettings ) {
            Coordinate oldLocation = locationSettings.Location;
            int oldIndex = IndexOfLocation( oldLocation );
            typeof( Settings ).Log( "Removing location settings " + oldIndex );

            var oldContainer = GetLocationSettingsContainer( oldIndex );
            string[] locationNames = LocationNames.Where( ( n, i ) => i != oldIndex ).ToArray( );

            if( locationSettings.Tile != ForecastDisplayMode.None ) {
                locationSettings.Tile = ForecastDisplayMode.None;
                locationSettings.UpdateTile( null, TileUpdateReason.Refresh );
            }

            _settings.RemoveAt( oldIndex );
            LocationNames = locationNames;
            oldContainer.Value = null;
            oldLocation.DeleteFile( typeof( Settings ), oldIndex );
        }

        /// <summary>Updates the tile agent, if it has not already been scheduled this launch.</summary>
        /// <see href="http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh202941.aspx"/>
        public static void UpdateTileAgent( ) {
            if( _tileAgentReset )
                return;

            _tileAgentReset = true;
            ResetTileAgent( );
        }


        #region Private Members

        private static readonly List<Setting<LocationSettings>> _settings = new List<Setting<LocationSettings>>( );
        private static readonly Dictionary<Coordinate, ForecastInfo> _agentSettings;
        private static readonly string[] EmptyNames = new string[0];

        private static Setting<Guid> _uniqueId;
        private static Setting<uint> _uniquifier;
        private static Setting<Units> _defaultUnits;
        private static Setting<int> _maximumTrialCount;
        private static Setting<string[]> _locationNames;
        private static Setting<uint> _tileBackgroundColor;
        private static Setting<bool> _hasPreviousForecasts;
        private static Setting<bool> _disableTileRescheduling;
        private static Setting<bool> _disableForecastAutoRefresh;


        static Settings( ) {
            typeof( Settings ).Log( "Initializing, v" + AssemblyInfo.Version + ", " + System.Threading.Thread.CurrentThread.CurrentCulture );

            // Migrate any existing single-location settings, if present.
            string locationString = MigrateSetting<string>( "location" );
            if( locationString != null ) {
                typeof( Settings ).Log( "Migrating v1 forecast" );
                Coordinate location = Coordinate.Parse( locationString );
                Units units = MigrateSetting<Units>( "units" );
                bool nightExtremes = MigrateSetting<bool>( "night_extremes" );

                string search = MigrateSetting<string>( "search" );
                string downloadUri = MigrateSetting<string>( "download_uri" );
                DateTimeOffset lastUpdate = MigrateSetting<DateTimeOffset>( "last_update" );

                ForecastDisplayMode tile = MigrateSetting<ForecastDisplayMode>( "tile" );
                string tileTitle = MigrateSetting<string>( "tile_title" );

                var locationSettings = new LocationSettings( location, search, ForecastSourceId.NOAA, downloadUri ) {
                    Units = units,
                    NightExtremes = nightExtremes,
                    LastUpdate = lastUpdate,
                    Tile = ForecastDisplayMode.None,
                    TileTitle = tileTitle,
                };

                AddLocation( locationSettings );
                DefaultUnits = units;
            }

            // Ensure maximum trial count is up to date.
            int minimumTrialCount = Math.Max( 2, Count );
            MaximumTrialCount = Math.Max( minimumTrialCount, MaximumTrialCount );

            // Reset main tile.
            var tiles = ShellTile.ActiveTiles.ToArray( );
            var tileData = new StandardTileData {
                BackgroundImage = SharedStorage.BackgroundImage,
                Title = AssemblyInfo.Title
            };
            tiles[0].Update( tileData );

            // If we have tiles without corresponding settings, try to restore settings from encoded information.
            int secondaryTileCount = tiles.Length - 1;
            if( Count < secondaryTileCount ) {
                typeof( Settings ).Log( "Recovering forecasts from tile data." );
                for( int i = 1; i < tiles.Length; ++i ) {
                    string tileUri = tiles[i].NavigationUri.ToString( );

                    var locationSettings = LocationSettings.FromTile( tileUri );
                    if( locationSettings == null )
                        typeof( Settings ).Log( "- Could not recover forecast for " + ForecastInfo.GetIdentifierFromTileUri( tileUri ) );
                    else if( IndexOfLocation( locationSettings.Location ) >= 0 )
                        typeof( Settings ).Log( "- Already have forecast for tile " + locationSettings.TileId );
                    else {
                        typeof( Settings ).Log( "- Recovering forecast for tile " + locationSettings.TileId );
                        AddLocation( locationSettings );
                    }
                }
            }

            // Get updated information from tile agent.
            typeof( Settings ).Log( "- Loading tile agent settings." );
            ForecastInfo[] forecastInfo;
            if( SharedStorage.TryLoadForecastInfo( out forecastInfo ) ) {
                _agentSettings = new Dictionary<Coordinate, ForecastInfo>( forecastInfo.Length );
                foreach( ForecastInfo info in forecastInfo )
                    _agentSettings[info.Location] = info;
            }
            else {
                _agentSettings = new Dictionary<Coordinate, ForecastInfo>( 0 );
            }
        }

        private static string[] LocationNames {
            get { return Ensure( ref _locationNames, "location_names" ).Value ?? EmptyNames; }
            set { Ensure( ref _locationNames, "location_names" ).Value = value; }
        }


        #region Tile Background Agent

        private static bool _tileAgentReset;
        private static PeriodicTask _tileAgent;

        private static void InvalidateTileAgentSettings( ) {
            _tileAgentReset = false;
        }

        private static void ResetTileAgent( ) {
            typeof( Settings ).Log( "Updating tile agent." );

            if( _tileAgent == null )
                try { _tileAgent = ScheduledActionService.Find( SharedStorage.AgentName ) as PeriodicTask; }
                catch( ArgumentException ex ) {
                    typeof( Settings ).Log( "Tile agent lookup failed: " + ex );
                    return;
                }

            if( _tileAgent != null )
                RemoveAgent( SharedStorage.AgentName );


            typeof( Settings ).Log( "- Saving tile agent settings." );
            ForecastInfo[] forecastInfo = new ForecastInfo[Count];
            for( int i = 0; i < forecastInfo.Length; ++i )
                forecastInfo[i] = GetLocationSettings( i ).GetForecastInfo( );
            SharedStorage.SaveForecastInfo( forecastInfo );


            typeof( Settings ).Log( "- Scheduling new tile agent." );
            _tileAgent = new PeriodicTask( SharedStorage.AgentName ) { Description = "WeatherSpark tile update task" };
            try {
                ScheduledActionService.Add( _tileAgent );
#if DEBUG_AGENT
                ScheduledActionService.LaunchForTest( SharedStorage.AgentName, TimeSpan.FromSeconds( 10 ) );
#endif
            }
            catch( InvalidOperationException ex ) {
                typeof( Settings ).Log( ex.ToString( ) );
                _tileAgent = null;

                if( ex.Message.Contains( "BNS Error: The action is disabled" ) ) {
                    System.Windows.MessageBox.Show( "Background agents for this application have been disabled by the user." );
                }
                else if( ex.Message.Contains( "BNS Error: The maximum number of ScheduledActions of this type have already been added." ) ) {
                    // No user action required. The system prompts the user when the hard limit of periodic tasks has been reached.
                }
            }
            catch( SchedulerServiceException ex ) {
                // No user action required.
                typeof( Settings ).Log( ex.ToString( ) );
                _tileAgent = null;
            }
        }

        private static void RemoveAgent( string name ) {
            typeof( Settings ).Log( "- Removing existing tile agent." );
            try {
                ScheduledActionService.Remove( name );
            }
            catch( Exception ex ) {
                typeof( Settings ).Log( ex.Message );
            }
        }

        #endregion

        #region Setting Values

        private static Setting<LocationSettings> GetLocationSettingsContainer( int index ) {
            // Load settings up to specified index.
            while( _settings.Count <= index ) {
                string name = LocationNames[_settings.Count];
                var container = CreateLocationSettingsContainer( name, null );
                _settings.Add( container );
            }

            return _settings[index];
        }

        private static void LoadAllSettingsContainers( ) {
            if( Count > 0 )
                GetLocationSettingsContainer( Count - 1 );
        }

        private static Setting<LocationSettings> CreateLocationSettingsContainer( string name, LocationSettings locationSettings ) {
            var container = new Setting<LocationSettings>( name );
            container.Value = locationSettings ?? container.Value;
            LocationSettings.InitializeLocation( container.Value, Coordinate.Parse( name ) );

            return container;
        }


        private static T MigrateSetting<T>( string key ) {
            T value;
            if( IsolatedStorageSettings.ApplicationSettings.TryGetValue<T>( key, out value ) )
                IsolatedStorageSettings.ApplicationSettings.Remove( key );

            return value;
        }

        private static Setting<T> Ensure<T>( ref Setting<T> setting, string name ) {
            if( setting == null )
                setting = new Setting<T>( name );
            return setting;
        }

        private static void OnLocationSettingsChanged( object sender, PropertyChangedEventArgs e ) {
            var locationSettings = (LocationSettings)sender;
            if( e.PropertyName == LocationSettings.UnitsPropertyName )
                DefaultUnits = locationSettings.Units;

            if( locationSettings.Tile != ForecastDisplayMode.None
             && LocationSettings.IsForecastInfoProperty( e.PropertyName ) )
                InvalidateTileAgentSettings( );

            typeof( LocationSettings ).Log( e.PropertyName + " value changed" );
            IsolatedStorageSettings.ApplicationSettings.Save( );
        }

        private static void OnSettingChanged<T>( T oldValue, T newValue ) {
            IsolatedStorageSettings.ApplicationSettings.Save( );

            var oldLocationSettings = oldValue as LocationSettings;
            if( oldLocationSettings != null )
                oldLocationSettings.PropertyChanged -= OnLocationSettingsChanged;

            OnSettingInitialized( newValue );
        }

        private static void OnSettingInitialized<T>( T value ) {
            var newLocationSettings = value as LocationSettings;
            if( newLocationSettings != null )
                newLocationSettings.PropertyChanged += OnLocationSettingsChanged;
        }

        private sealed class Setting<T> {
            private readonly string _name;
            private T _value;

            public Setting( string name ) {
                this._name = name;

                if( IsolatedStorageSettings.ApplicationSettings.TryGetValue( name, out this._value ) )
                    Settings.OnSettingInitialized( this._value );
            }

            public T Value {
                get { return this._value; }
                set {
                    if( !object.Equals( this._value, value ) ) {
                        T oldValue = this._value;
                        this._value = value;

                        if( value == null )
                            IsolatedStorageSettings.ApplicationSettings.Remove( this._name );
                        else
                            IsolatedStorageSettings.ApplicationSettings[this._name] = value;

                        string name = char.IsDigit( this._name, 1 ) ? "location " + Array.IndexOf( LocationNames, this._name ) : this._name;
                        typeof( Settings ).Log( name + " value changed" );
                        Settings.OnSettingChanged( oldValue, value );
                    }
                }
            }

#if DEBUG
            public override string ToString( ) {
                return string.Format( "{0}: {1}", this._name, (object)this._value ?? "<null>" );
            }
#endif
        }

        #endregion

        #endregion
    }

}
